"""
OpenGL 点云查看器（PySide2 + pyqtgraph.opengl）

目标：
- 使用 GPU 加速的三维散点渲染，流畅显示百万级点云。
- 提供文件管理面板（添加文件、显示开关、颜色编辑）、交互（旋转/缩放/平移）、颜色映射与点大小调节。
- 保持原模块外部接口一致（导出 `PCDViewerWindow` 与 `PCDLoader`），以便与既有窗口集成。

安全与约束：
- Python 3.10 与 PySide2；仅支持 ASCII PCD，限制每文件点数避免内存溢出；异常防护与输入校验。
- 代码遵循 PEP8，中文注释，并避免与 PyQt5 混用。
"""

from __future__ import annotations

from pathlib import Path
import os
import zlib
from typing import Dict, List, Optional, Tuple

import numpy as np
from PySide2.QtCore import Qt, QTimer
from PySide2.QtGui import QColor
from PySide2.QtWidgets import (
    QAbstractItemView,
    QColorDialog,
    QFileDialog,
    QComboBox,
    QFrame,
    QHBoxLayout,
    QLabel,
    QMainWindow,
    QPushButton,
    QSlider,
    QSplitter,
    QStatusBar,
    QTableWidget,
    QTableWidgetItem,
    QVBoxLayout,
    QWidget,
    QHeaderView,
)
from gglobal import logger
from .language_manager import LanguageManager  # 引入语言管理器以支持中英文

# OpenGL 可用性标记（供测试与外部模块识别）
_GL_AVAILABLE: bool = False

# 强制使用 OpenGL 渲染：若不可用则给出友好错误信息，但不在导入阶段抛出致命异常
try:
    import pyqtgraph.opengl as gl  # type: ignore
    _GL_AVAILABLE = True
    logger.info("pcd_viewer: pyqtgraph.opengl imported successfully")
except Exception as exc:  # noqa: F401
    _GL_AVAILABLE = False
    logger.warning("pcd_viewer: pyqtgraph.opengl import failed: %s", exc)
    # 延迟错误：在窗口构造时提示安装而非模块导入阶段中断
    gl = None  # type: ignore

# OpenGL环境诊断
logger.info("pcd_viewer: QT_OPENGL=%s", os.environ.get("QT_OPENGL", "not set"))
logger.info("pcd_viewer: QT_ANGLE_PLATFORM=%s", os.environ.get("QT_ANGLE_PLATFORM", "not set"))


# 默认像素模式开关（环境变量控制）：MOOETOOL_GL_PXMODE=0 切换为非像素模式
_DEFAULT_PXMODE: bool = os.getenv("MOOETOOL_GL_PXMODE", "1") != "0"
logger.info("pcd_viewer: GL available=%s, default pxMode=%s", _GL_AVAILABLE, _DEFAULT_PXMODE)



class PCDLoader:
    """PCD 文件加载器（支持 ASCII 与二进制 PCD）"""

    # 每文件最大点数硬限制（超过则采样提前停止）
    MAX_POINTS: int = 2_000_000

    @staticmethod
    def load_pcd(file_path: str) -> List[Tuple[float, float, float]]:
        p = Path(file_path)
        if not p.exists() or p.suffix.lower() != ".pcd":
            raise ValueError("文件不存在或后缀非 .pcd")

        logger.info("PCDLoader: 开始解析文件 %s", str(p))

        data_type: Optional[str] = None
        fields: List[str] = []
        sizes: List[int] = []
        types_: List[str] = []
        counts: List[int] = []
        width: int = 0
        height: int = 1
        points_declared: Optional[int] = None
        header_end: int = 0

        with p.open("rb") as f:
            header_lines: List[str] = []
            while True:
                bline = f.readline()
                if not bline:
                    break
                sline = bline.decode("utf-8", errors="replace").strip()
                if not sline or sline.startswith("#"):
                    continue
                header_lines.append(sline)
                if sline.upper().startswith("DATA"):
                    parts = sline.split()
                    data_type = parts[1].lower() if len(parts) >= 2 else None
                    header_end = f.tell()
                    break

            for ln in header_lines:
                up = ln.upper()
                if up.startswith("FIELDS"):
                    fields = ln.split()[1:]
                elif up.startswith("SIZE"):
                    try:
                        sizes = [int(x) for x in ln.split()[1:]]
                    except Exception:
                        sizes = []
                elif up.startswith("TYPE"):
                    types_ = ln.split()[1:]
                elif up.startswith("COUNT"):
                    try:
                        counts = [int(x) for x in ln.split()[1:]]
                    except Exception:
                        counts = []
                elif up.startswith("WIDTH"):
                    try:
                        width = int(ln.split()[1])
                    except Exception:
                        width = 0
                elif up.startswith("HEIGHT"):
                    try:
                        height = int(ln.split()[1])
                    except Exception:
                        height = 1
                elif up.startswith("POINTS"):
                    try:
                        points_declared = int(ln.split()[1])
                    except Exception:
                        points_declared = None

            if not fields:
                raise ValueError("PCD FIELDS 缺失")
            if not counts or len(counts) != len(fields):
                counts = [1] * len(fields)
            if not sizes or len(sizes) != len(fields):
                sizes = [4] * len(fields)
            if not types_ or len(types_) != len(fields):
                types_ = ["F"] * len(fields)

            def idx(name: str) -> int:
                try:
                    return fields.index(name)
                except ValueError:
                    return -1

            xi, yi, zi = idx("x"), idx("y"), idx("z")
            if min(xi, yi, zi) < 0:
                raise ValueError("PCD FIELDS 中未包含 x/y/z")

            def map_dtype(t: str, s: int) -> str:
                t = t.upper()
                if t == "F":
                    return "<f4" if s == 4 else "<f8"
                if t == "I":
                    return {1: "<i1", 2: "<i2", 4: "<i4", 8: "<i8"}.get(s, "<i4")
                if t == "U":
                    return {1: "<u1", 2: "<u2", 4: "<u4", 8: "<u8"}.get(s, "<u4")
                return "<f4"

            dtype_fields = []
            for i in range(len(fields)):
                dt = np.dtype(map_dtype(types_[i], sizes[i]))
                c = int(counts[i]) if i < len(counts) else 1
                if c == 1:
                    dtype_fields.append((fields[i], dt))
                else:
                    dtype_fields.append((fields[i], dt, c))
            rec_dtype = np.dtype(dtype_fields)

            if data_type == "ascii":
                points: List[Tuple[float, float, float]] = []
                with p.open("r", encoding="utf-8", errors="replace") as tf:
                    header_done = False
                    for raw in tf:
                        line = raw.strip()
                        if not line or line.startswith("#"):
                            continue
                        if not header_done:
                            if line.upper().startswith("DATA"):
                                header_done = True
                            continue
                        cols = line.split()
                        if not cols:
                            continue
                        try:
                            x = float(cols[xi])
                            y = float(cols[yi])
                            z = float(cols[zi])
                        except Exception:
                            continue
                        points.append((x, y, z))
                        if len(points) >= PCDLoader.MAX_POINTS:
                            break
                if not points:
                    raise ValueError("未解析到有效点数据")
                return points

            if data_type in ("binary", "binary_compressed"):
                with p.open("rb") as bf:
                    bf.seek(header_end)
                    data = bf.read()
                if data_type == "binary_compressed":
                    if len(data) < 8:
                        raise ValueError("binary_compressed 数据不足")
                    try:
                        c_size = int.from_bytes(data[0:4], "little", signed=False)
                        u_size = int.from_bytes(data[4:8], "little", signed=False)
                    except Exception:
                        raise ValueError("binary_compressed 头解析失败")
                    if 8 + c_size > len(data):
                        raise ValueError("binary_compressed 尺寸不匹配")
                    comp = data[8:8 + c_size]
                    try:
                        decompressed = zlib.decompress(comp)
                    except Exception:
                        raise ValueError("binary_compressed 解压失败")
                    if u_size and len(decompressed) != u_size:
                        raise ValueError("binary_compressed 解压后尺寸不匹配")
                    data_buf = decompressed
                else:
                    data_buf = data

                try:
                    recs = np.frombuffer(data_buf, dtype=rec_dtype)
                except Exception:
                    raise ValueError("二进制数据解析失败")

                if points_declared and recs.shape[0] > points_declared:
                    recs = recs[:points_declared]
                if recs.shape[0] == 0:
                    raise ValueError("未解析到有效点数据")

                def get_field(arr: np.ndarray, name: str) -> np.ndarray:
                    try:
                        v = arr[name]
                        if np.ndim(v) == 2:
                            v = v[:, 0]
                        return np.asarray(v, dtype=np.float32)
                    except Exception:
                        return np.zeros(arr.shape[0], dtype=np.float32)

                xs = get_field(recs, fields[xi])
                ys = get_field(recs, fields[yi])
                zs = get_field(recs, fields[zi])
                pts_arr = np.stack([xs, ys, zs], axis=1).astype(np.float32, copy=False)

                try:
                    mins = pts_arr.min(axis=0)
                    maxs = pts_arr.max(axis=0)
                    vars_ = pts_arr.var(axis=0)
                    logger.info(
                        "PCDLoader: 完成解析，共 %d 点，范围 x[%.3f,%.3f] y[%.3f,%.3f] z[%.3f,%.3f] 方差 x=%.6f y=%.6f z=%.6f",
                        pts_arr.shape[0], mins[0], maxs[0], mins[1], maxs[1], mins[2], maxs[2], vars_[0], vars_[1], vars_[2]
                    )
                except Exception:
                    pass

                return [(float(x), float(y), float(z)) for x, y, z in pts_arr]

            raise ValueError("未知的 PCD DATA 类型")


def _qcolor_to_rgba(color: QColor) -> Tuple[float, float, float, float]:
    """将 QColor 转为 (r,g,b,a) 0-1 浮点颜色。"""
    return (color.red() / 255.0, color.green() / 255.0, color.blue() / 255.0, 1.0)


class GLPointCloudEntry:
    """GL 点云条目封装：scatter 项与颜色映射。"""

    def __init__(self, points: List[Tuple[float, float, float]], color: QColor, size: float = 3.0) -> None:
        # 使用 float32 的 NumPy 数组以降低内存与加速上传，并过滤非法值
        logger.info("GLEntry: 输入点数量=%d", len(points))
        arr = np.asarray(points, dtype=np.float32)
        if arr.ndim != 2 or arr.shape[1] < 3:
            arr = arr.reshape(-1, 3)
        # 过滤 NaN/Inf 数据，保证绘制稳定性
        finite_mask = np.isfinite(arr).all(axis=1)
        arr = arr[finite_mask]
        # 保证内存连续，避免驱动上传异常
        self.points = np.ascontiguousarray(arr[:, :3], dtype=np.float32)
        try:
            mins = self.points.min(axis=0)
            maxs = self.points.max(axis=0)
            vars_ = self.points.var(axis=0)
            logger.info(
                "GLEntry: 过滤后点=%d  范围 x[%.3f,%.3f] y[%.3f,%.3f] z[%.3f,%.3f]  方差 x=%.6f y=%.6f z=%.6f",
                self.points.shape[0], mins[0], maxs[0], mins[1], maxs[1], mins[2], maxs[2], vars_[0], vars_[1], vars_[2]
            )
        except Exception:
            pass
        self._base_color = QColor(color)
        self._size = float(size)
        # 颜色模式内部使用英文键以避免受本地化文本影响
        self._mode = "uniform"
        self.scatter = gl.GLScatterPlotItem(
            pos=self.points,
            size=self._size,
            color=_qcolor_to_rgba(self._base_color),
            # 默认像素模式（可通过环境变量关闭），便于观察是否“合成一个点”的问题来源
            pxMode=_DEFAULT_PXMODE,
        )
        logger.info(
            "GLEntry: 创建 scatter pos.shape=%s dtype=%s pxMode=%s size=%.2f",
            str(self.points.shape), str(self.points.dtype), _DEFAULT_PXMODE, self._size
        )

    def set_visible(self, visible: bool) -> None:
        try:
            self.scatter.setVisible(bool(visible))
        except Exception:
            pass

    def set_color(self, color: QColor) -> None:
        try:
            self._base_color = QColor(color)
            if self._mode == "uniform":
                self.scatter.setData(color=_qcolor_to_rgba(self._base_color))
            else:
                self.apply_color_map(self._mode)
        except Exception:
            pass

    def set_size(self, size: float) -> None:
        try:
            self._size = float(max(1.0, size))
            self.scatter.setData(size=self._size)
            logger.info("GLEntry: 更新点大小=%.2f", self._size)
        except Exception:
            pass

    def apply_color_map(self, mode: str) -> None:
        """颜色映射：支持统一颜色（uniform）与按Z映射（map_z）。"""
        try:
            self._mode = mode
            if mode == "map_z":
                z = self.points[:, 2]
                z_min = float(z.min())
                z_max = float(z.max())
                if z_max - z_min < 1e-12:
                    colors = np.tile(np.array(_qcolor_to_rgba(self._base_color), dtype=np.float32), (self.points.shape[0], 1))
                else:
                    norm = (z - z_min) / (z_max - z_min)
                    # 简单梯度（蓝→青→绿→黄→红）
                    r = norm
                    g = 1.0 - np.clip(norm - 0.5, 0.0, 1.0)
                    b = 1.0 - norm
                    a = np.ones_like(norm)
                    colors = np.stack([r, g, b, a], axis=1).astype(np.float32)
                self.scatter.setData(color=np.ascontiguousarray(colors, dtype=np.float32))
                logger.info("GLEntry: 应用按Z映射 colors.shape=%s", str(colors.shape))
            else:
                self.scatter.setData(color=_qcolor_to_rgba(self._base_color))
                logger.info("GLEntry: 应用统一颜色=%s", self._base_color.name())
        except Exception:
            pass


class PCDViewerWindow(QMainWindow):
    """OpenGL 点云查看器主窗口。"""

    def __init__(self, parent: Optional[QWidget] = None) -> None:
        super().__init__(parent)
        # 语言管理器：用于界面文本的本地化
        self.language_manager = LanguageManager()
        # 使用语言键设置窗口标题（支持语言切换）
        self.setWindowTitle(self.language_manager.get_text("pcd_viewer_window_title", "PCD 查看器"))
        self.setMinimumSize(900, 600)
        self.setAttribute(Qt.WA_DeleteOnClose, True)

        # 兼容外部测试的标记
        self._use_gl: bool = _GL_AVAILABLE

        # 界面分割器
        splitter = QSplitter(Qt.Horizontal, self)
        self.setCentralWidget(splitter)

        # 左侧文件管理面板
        left_panel = QFrame(self)
        left_panel.setMinimumWidth(300)
        left_layout = QVBoxLayout(left_panel)
        left_layout.setContentsMargins(8, 8, 8, 8)
        left_layout.setSpacing(8)

        # 添加文件按钮
        self.btn_add = QPushButton(self.language_manager.get_text("pcd_viewer_add_button", "添加文件"), left_panel)
        left_layout.addWidget(self.btn_add)

        # 点大小控制
        self.lbl_size = QLabel(self.language_manager.get_text("pcd_viewer_point_size_label", "点大小"), left_panel)
        self.size_slider = QSlider(Qt.Horizontal, left_panel)
        self.size_slider.setRange(1, 10)
        self.size_slider.setValue(5)
        left_layout.addWidget(self.lbl_size)
        left_layout.addWidget(self.size_slider)

        # 颜色映射选择（统一颜色 / 按Z映射）
        self.lbl_cmap = QLabel(self.language_manager.get_text("pcd_viewer_color_map_label", "颜色映射"), left_panel)
        self.color_map_combo = QComboBox(left_panel)
        # 使用语言键注册下拉框选项（统一颜色 / 按Z映射）
        self.language_manager.register_combo_box(self.color_map_combo, "pcd_viewer_color_map_items")
        left_layout.addWidget(self.lbl_cmap)
        left_layout.addWidget(self.color_map_combo)

        # 文件列表：显示开关、文件名、颜色
        self.table = QTableWidget(left_panel)
        self.table.setColumnCount(3)
        # 表头使用语言键获取（列表）
        headers = self.language_manager.get_text("pcd_viewer_table_headers", ["显示", "文件名", "颜色"])
        try:
            if isinstance(headers, list) and len(headers) == 3:
                self.table.setHorizontalHeaderLabels(headers)
            else:
                self.table.setHorizontalHeaderLabels(["显示", "文件名", "颜色"])
        except Exception:
            self.table.setHorizontalHeaderLabels(["显示", "文件名", "颜色"])
        self.table.horizontalHeader().setSectionResizeMode(0, QHeaderView.ResizeToContents)
        self.table.horizontalHeader().setSectionResizeMode(1, QHeaderView.Stretch)
        self.table.horizontalHeader().setSectionResizeMode(2, QHeaderView.ResizeToContents)
        self.table.setEditTriggers(QAbstractItemView.NoEditTriggers)
        self.table.setSelectionBehavior(QAbstractItemView.SelectRows)
        self.table.setSelectionMode(QAbstractItemView.SingleSelection)
        left_layout.addWidget(self.table, stretch=1)

        splitter.addWidget(left_panel)

        # 右侧 OpenGL 视图
        right_panel = QFrame(self)
        right_layout = QHBoxLayout(right_panel)
        right_layout.setContentsMargins(0, 0, 0, 0)
        right_layout.setSpacing(0)
        if not _GL_AVAILABLE:
            # 若 GL 不可用，提示用户安装依赖（适配中英文）
            msg = QLabel(
                self.language_manager.get_text(
                    "pcd_viewer_gl_unavailable_message",
                    "pyqtgraph.opengl 未安装或不可用，请在 conda base 环境中安装：<br><code>conda install -c conda-forge pyqtgraph</code>"
                ),
                right_panel,
            )
            msg.setTextFormat(Qt.RichText)
            msg.setAlignment(Qt.AlignCenter)
            right_layout.addWidget(msg)
            # 注册以便语言切换时自动更新提示文本
            self.language_manager.register_widget(msg, "pcd_viewer_gl_unavailable_message")
        else:
            self.gl_view = gl.GLViewWidget(right_panel)
            logger.info("pcd_viewer: GLViewWidget created successfully")
            
            # OpenGL上下文诊断
            try:
                self.gl_view.makeCurrent()
                logger.info("pcd_viewer: OpenGL context made current successfully")
                
                # 延迟获取OpenGL信息，确保上下文完全初始化
                def delayed_opengl_info():
                    try:
                        import OpenGL.GL as ogl
                        # 检查是否有OpenGL错误
                        ogl.glGetError()  # 清除之前的错误
                        
                        version = ogl.glGetString(ogl.GL_VERSION)
                        vendor = ogl.glGetString(ogl.GL_VENDOR)
                        renderer = ogl.glGetString(ogl.GL_RENDERER)
                        
                        if version:
                            logger.info("pcd_viewer: OpenGL version=%s", version.decode())
                        if vendor:
                            logger.info("pcd_viewer: OpenGL vendor=%s", vendor.decode())
                        if renderer:
                            logger.info("pcd_viewer: OpenGL renderer=%s", renderer.decode())
                            
                        # 再次检查错误
                        error = ogl.glGetError()
                        if error != ogl.GL_NO_ERROR:
                            logger.warning("pcd_viewer: OpenGL error after getting strings: %s", error)
                    except Exception as gl_exc:
                        logger.warning("pcd_viewer: Failed to get OpenGL info: %s", gl_exc)
                
                # 延迟执行OpenGL信息获取
                QTimer.singleShot(500, delayed_opengl_info)
                
                self.gl_view.doneCurrent()
            except Exception as ctx_exc:
                logger.error("pcd_viewer: Failed to make OpenGL context current: %s", ctx_exc)
            
            # 为避免关闭后重开导致旧着色器缓存复用（program ID 在新上下文无效），
            # 在创建 GL 视图后重置 pyqtgraph 的着色器缓存。
            try:
                self._reset_shader_cache()
                logger.info("pcd_viewer: Shader cache reset successfully")
            except Exception as reset_exc:
                logger.warning("pcd_viewer: Failed to reset shader cache: %s", reset_exc)
            # 统一比例因子：默认将视距缩小 10 倍（便于小尺度点云的初始观察）
            self._scale_factor: float = 0.1
            base_distance = 600.0
            self.gl_view.opts["distance"] = base_distance * self._scale_factor
            self.gl_view.opts["fov"] = 60.0
            # 设置相机姿态：保持 Z 轴为默认向上（GLView 默认 Z 轴向上）
            self.gl_view.setCameraPosition(
                distance=self.gl_view.opts["distance"], elevation=25.0, azimuth=45.0
            )
            self.gl_view.setBackgroundColor("#1e1e1e")
            right_layout.addWidget(self.gl_view)
            logger.info(
                "PCDViewerWindow: 已创建 GLViewWidget 距离=%.1f fov=%.1f scale=%.1f",
                self.gl_view.opts.get("distance", 0.0), self.gl_view.opts.get("fov", 0.0), self._scale_factor,
            )
            
            # 延迟执行OpenGL诊断，确保窗口完全初始化
            try:
                QTimer.singleShot(1500, self._diagnose_opengl_state)
            except Exception as timer_exc:
                logger.warning("PCDViewerWindow: Failed to schedule OpenGL diagnosis: %s", timer_exc)
        splitter.addWidget(right_panel)
        splitter.setStretchFactor(0, 0)
        splitter.setStretchFactor(1, 1)

        # 添加坐标轴与网格（便于空间感知）：X 红，Y 绿，Z 蓝
        if _GL_AVAILABLE:
            # 按照缩放因子同步缩小坐标轴与网格尺寸，保证整体视觉一致
            _sf = float(getattr(self, "_scale_factor", 1.0))
            visual_scale = _sf
            axes = self._create_gl_axes(length=100.0 * visual_scale)
            logger.info(f"PCDViewerWindow: 创建坐标轴完成，数量={len(axes)}")
            
            successful_axes = 0
            for i, ax in enumerate(axes):
                try:
                    self.gl_view.addItem(ax)
                    successful_axes += 1
                    logger.info(f"PCDViewerWindow: 成功添加坐标轴 {i+1}/{len(axes)}")
                except Exception as ax_add_exc:
                    logger.error(f"PCDViewerWindow: 添加坐标轴 {i+1} 失败: {ax_add_exc}")
                    
            self._axes_items = axes  # 记录以便关闭窗口时清理
            logger.info(f"PCDViewerWindow: 坐标轴添加完成，成功数量={successful_axes}")
            try:
                grid = gl.GLGridItem()
                logger.info(f"PCDViewerWindow: 创建 GLGridItem 成功")
                
                # 保持网格在 X-Y 平面，使 Z 轴默认向上；
                # 初始网格的尺寸与间距由 visual_scale 决定，其中 visual_scale = self._scale_factor（默认为 0.1）。
                # 因此：
                #   - 网格总尺寸 = 200 * visual_scale（此处为 20 单位）
                #   - 网格间距 = 10 * visual_scale（此处为 1 单位）
                # 该初始设置用于在尚未加载点云数据时提供可见的参考栅格；
                # 加载点云后会在 _apply_scene_scale() 中按点云范围自适应调整。
                
                try:
                    grid_size = 200 * visual_scale
                    grid_spacing = 10 * visual_scale
                    logger.info(f"PCDViewerWindow: 设置网格参数 - size={grid_size:.3f}, spacing={grid_spacing:.3f}")
                    grid.setSize(grid_size, grid_size)
                    grid.setSpacing(grid_spacing, grid_spacing)
                    logger.info(f"PCDViewerWindow: 网格参数设置成功")
                except Exception as grid_setup_exc:
                    logger.error(f"PCDViewerWindow: 网格参数设置失败: {grid_setup_exc}")
                    # 使用默认参数
                    grid.setSize(20, 20)
                    grid.setSpacing(1, 1)
                    logger.info(f"PCDViewerWindow: 使用默认网格参数")
                
                self.gl_view.addItem(grid)
                self._grid_item = grid
                logger.info("PCDViewerWindow: 已添加坐标轴与网格 (Z 轴向上) scale_factor=%.3f visual_scale=%.3f", _sf, visual_scale)
                
            except Exception as grid_create_exc:
                logger.error(f"PCDViewerWindow: 创建网格失败: {grid_create_exc}")
                self._grid_item = None

        # 状态栏
        self.status = QStatusBar(self)
        self.setStatusBar(self.status)

        # 事件连接
        self.btn_add.clicked.connect(self._on_add_files_clicked)
        self.table.itemChanged.connect(self._on_table_item_changed)
        self.table.cellClicked.connect(self._on_table_cell_clicked)
        self.size_slider.valueChanged.connect(self._on_point_size_changed)
        self.color_map_combo.currentIndexChanged.connect(self._on_color_map_changed)

        # 注册静态控件以支持语言切换自动更新
        try:
            self.language_manager.register_widget(self, "pcd_viewer_window_title", property_name="windowTitle")
            self.language_manager.register_widget(self.btn_add, "pcd_viewer_add_button")
            self.language_manager.register_widget(self.lbl_size, "pcd_viewer_point_size_label")
            self.language_manager.register_widget(self.lbl_cmap, "pcd_viewer_color_map_label")
        except Exception:
            pass

        # 订阅语言变化事件（更新表头、窗口标题与状态栏文本）
        try:
            self.language_manager.language_changed.connect(self._on_language_changed)
        except Exception:
            pass

        # 行索引到 GL 条目的映射
        self._row_to_gl: Dict[int, GLPointCloudEntry] = {}
        self._row_to_path: Dict[int, str] = {}

        # 颜色池（循环使用）
        self._colors: List[QColor] = [
            QColor(255, 99, 71),   # Tomato
            QColor(30, 144, 255),  # DodgerBlue
            QColor(60, 179, 113),  # MediumSeaGreen
            QColor(218, 165, 32),  # GoldenRod
            QColor(123, 104, 238), # MediumSlateBlue
            QColor(255, 105, 180), # HotPink
            QColor(0, 191, 255),   # DeepSkyBlue
        ]
        self._color_idx = 0

    # ------------------------ 私有方法 ------------------------
    def _resolve_default_download_dir(self) -> Path:
        """解析本地默认下载目录，并在必要时创建。

        优先使用用户主目录下的 `Downloads` 或中文 `下载` 目录；
        若两者均不可用，则回退到项目内的 `resource/pcd_download` 目录。
        """
        try:
            home = Path.home()
            candidates = [home / "Downloads", home / "下载"]
            for c in candidates:
                try:
                    c.mkdir(parents=True, exist_ok=True)
                    return c
                except Exception:
                    # 目录创建失败则尝试下一个候选
                    pass
        except Exception:
            # Path.home() 异常则回退到项目目录
            pass

        fallback = Path("resource/pcd_download").resolve()
        try:
            fallback.mkdir(parents=True, exist_ok=True)
        except Exception:
            # 若仍失败，则保持原路径返回（QFileDialog 会自行处理不可用路径）
            pass
        return fallback

    def _create_gl_axes(self, length: float = 100.0) -> List[gl.GLLinePlotItem]:
        """创建三维坐标轴（X 红，Y 绿，Z 蓝）。"""
        try:
            logger.info(f"PCDViewerWindow: 创建坐标轴，长度={length:.3f}")
            
            pts_x = np.array([[0.0, 0.0, 0.0], [length, 0.0, 0.0]], dtype=np.float32)
            pts_y = np.array([[0.0, 0.0, 0.0], [0.0, length, 0.0]], dtype=np.float32)
            pts_z = np.array([[0.0, 0.0, 0.0], [0.0, 0.0, length]], dtype=np.float32)
            
            try:
                x_item = gl.GLLinePlotItem(pos=pts_x, color=(1.0, 0.0, 0.0, 1.0), width=3.0, antialias=True)
                logger.info(f"PCDViewerWindow: X轴创建成功")
            except Exception as x_exc:
                logger.error(f"PCDViewerWindow: X轴创建失败: {x_exc}")
                x_item = None
                
            try:
                y_item = gl.GLLinePlotItem(pos=pts_y, color=(0.0, 1.0, 0.0, 1.0), width=3.0, antialias=True)
                logger.info(f"PCDViewerWindow: Y轴创建成功")
            except Exception as y_exc:
                logger.error(f"PCDViewerWindow: Y轴创建失败: {y_exc}")
                y_item = None
                
            try:
                z_item = gl.GLLinePlotItem(pos=pts_z, color=(0.0, 0.5, 1.0, 1.0), width=3.0, antialias=True)
                logger.info(f"PCDViewerWindow: Z轴创建成功")
            except Exception as z_exc:
                logger.error(f"PCDViewerWindow: Z轴创建失败: {z_exc}")
                z_item = None
                
            # 过滤掉创建失败的轴
            axes = [item for item in [x_item, y_item, z_item] if item is not None]
            logger.info(f"PCDViewerWindow: 坐标轴创建完成，成功数量={len(axes)}")
            return axes
            
        except Exception as exc:
            logger.error(f"PCDViewerWindow: 坐标轴创建异常: {exc}")
            return []

    def _get_scene_bounds(self) -> Optional[Tuple[np.ndarray, np.ndarray]]:
        try:
            if not self._row_to_gl:
                return None
            mins = []
            maxs = []
            for entry in self._row_to_gl.values():
                try:
                    pts = entry.points
                    mins.append(pts.min(axis=0))
                    maxs.append(pts.max(axis=0))
                except Exception:
                    continue
            if not mins or not maxs:
                return None
            mn = np.min(np.stack(mins, axis=0), axis=0)
            mx = np.max(np.stack(maxs, axis=0), axis=0)
            return mn, mx
        except Exception:
            return None

    def _apply_scene_scale(self, center: np.ndarray, radius: float) -> None:
        try:
            rad = float(max(1.0, min(radius, 1e6)))
            dist = max(10.0, rad * 3.0)
            try:
                import pyqtgraph as pg  # type: ignore
                cvec = pg.Vector(float(center[0]), float(center[1]), float(center[2]))
                self.gl_view.setCameraPosition(pos=cvec, distance=dist)
            except Exception:
                self.gl_view.setCameraPosition(distance=dist)

            try:
                for axis in getattr(self, "_axes_items", []):
                    try:
                        self.gl_view.removeItem(axis)
                    except Exception:
                        pass
            except Exception:
                pass
            self._axes_items = self._create_gl_axes(length=rad)
            for ax in self._axes_items:
                self.gl_view.addItem(ax)

            if getattr(self, "_grid_item", None) is None:
                try:
                    grid = gl.GLGridItem()
                    self._grid_item = grid
                    self.gl_view.addItem(grid)
                except Exception:
                    pass
            try:
                # 法一：
                # 基本思想：
                #   - 基准单元长度 = 1.0（米）
                #   - 实际单元间距 = 基准单元长度 * self._scale_factor
                #   - 为覆盖整体范围（直径约 2*rad），将总尺寸对齐为单元间距的整数倍
                #   - 若单元数量过多（>200），自动增大间距以限制绘制负载
                # base_unit_m = 1.0
                # base_spacing = float(max(0.001, base_unit_m * float(getattr(self, "_scale_factor", 0.1))))
                # extent = rad * 2.0
                # cells = int(np.ceil(extent / base_spacing))
                # max_cells = 200
                # if cells > max_cells:
                #     spacing = extent / float(max_cells)
                #     cells = max_cells
                # else:
                #     spacing = base_spacing
                # size = float(cells) * float(spacing)
                # # 将栅格中心平移到点云中心的XY位置，便于覆盖当前数据范围
                # try:
                #     self._grid_item.resetTransform()
                #     self._grid_item.translate(float(center[0]), float(center[1]), 0.0)
                # except Exception:
                #     pass


                # 法二：
                # 自适应栅格：根据点云范围（半径 rad）计算栅格尺寸与间距
                # 设计目标：在直径约 2*rad 的范围内生成约 20 个网格单元，
                #   - 间距 spacing ≈ rad / 10（最小为 1，避免过密导致显示不清）
                #   - 总尺寸 size ≈ 2 * rad（覆盖整片点云范围）
                spacing = max(rad / 10.0, 1.0)
                size = rad * 2.0
                
                # 增强错误处理和日志记录
                try:
                    logger.info(f"PCDViewerWindow: 更新网格 - size={size:.3f}, spacing={spacing:.3f}")
                    self._grid_item.setSize(size, size)
                    self._grid_item.setSpacing(spacing, spacing)
                    logger.info(f"PCDViewerWindow: 网格更新成功")
                except Exception as grid_exc:
                    logger.error(f"PCDViewerWindow: 网格更新失败: {grid_exc}")
                    # 尝试基本的网格设置
                    try:
                        self._grid_item.setSize(100, 100)
                        self._grid_item.setSpacing(10, 10)
                        logger.info(f"PCDViewerWindow: 使用默认网格设置")
                    except Exception as fallback_exc:
                        logger.error(f"PCDViewerWindow: 默认网格设置也失败: {fallback_exc}")
            except Exception as exc:
                logger.error(f"PCDViewerWindow: 网格缩放异常: {exc}")
            try:
                # 法一：
                # 1m根据伸缩因子自适应调整栅格间距
                # logger.info(
                #     "PCDViewerWindow: 更新场景 center=(%.3f,%.3f,%.3f) radius=%.3f distance=%.3f grid_size=%.3f grid_spacing=%.3f",
                #     center[0], center[1], center[2], rad, dist, size, spacing,
                # )


                # 法二：
                logger.info(
                    "PCDViewerWindow: 更新场景 center=(%.3f,%.3f,%.3f) radius=%.3f distance=%.3f",
                    center[0], center[1], center[2], rad, dist,
                )
            except Exception:
                pass
        except Exception as exc:
            logger.warning("PCDViewerWindow: 更新场景失败: %s", exc)

    def _update_scene_after_data_change(self) -> None:
        try:
            bounds = self._get_scene_bounds()
            if bounds is None:
                return
            mn, mx = bounds
            center = (mn + mx) / 2.0
            extent = mx - mn
            radius = float(max(extent[0], extent[1], extent[2])) * 0.5
            self._apply_scene_scale(center, radius)
        except Exception:
            pass

    # ------------------------ 事件处理 ------------------------
    def _on_add_files_clicked(self) -> None:
        """选择并加载多个 .pcd 文件，建立 GL 散点条目。"""
        try:
            # 默认打开路径：本地下载目录（Downloads/下载），不可用时回退到项目内资源目录
            start_dir = str(self._resolve_default_download_dir())
            # 本地化文件对话框标题与过滤器
            dlg_title = self.language_manager.get_constant("pcd_viewer_file_dialog_title") or self.language_manager.get_text("pcd_viewer_file_dialog_title", "选择 PCD 文件")
            file_filter = self.language_manager.get_constant("pcd_viewer_file_filter") or self.language_manager.get_text("pcd_viewer_file_filter", "PCD 文件 (*.pcd)")
            paths, _ = QFileDialog.getOpenFileNames(self, dlg_title, start_dir, file_filter)
            if not paths:
                return
            self.add_files(paths)
        except Exception as exc:
            err_format = self.language_manager.get_constant("pcd_viewer_add_files_failed") or "添加文件失败：{error}"
            self.status.showMessage(err_format.format(error=exc), 5000)

    def closeEvent(self, event) -> None:
        """窗口关闭事件（硬关闭）：执行确定性清理并允许窗口真正销毁。

        安全策略：
        - 激活 GL 上下文后移除所有 GL 项（散点、坐标轴、网格）
        - 清空内部映射与表格并断开相关信号，释放资源
        - 不隐藏、不忽略事件，交由 Qt 正常销毁（WA_DeleteOnClose）
        """
        try:
            if _GL_AVAILABLE and hasattr(self, "gl_view"):
                # 尝试激活 GL 上下文，确保移除项时上下文有效
                try:
                    self.gl_view.makeCurrent()
                except Exception:
                    pass

                # 移除所有点云散点项（释放显存/内存）
                for entry in list(self._row_to_gl.values()):
                    try:
                        self.gl_view.removeItem(entry.scatter)
                    except Exception:
                        pass

                # 移除坐标轴
                try:
                    for axis in getattr(self, "_axes_items", []):
                        try:
                            self.gl_view.removeItem(axis)
                        except Exception:
                            pass
                    self._axes_items = []
                except Exception:
                    pass

                # 移除网格
                try:
                    grid_item = getattr(self, "_grid_item", None)
                    if grid_item is not None:
                        try:
                            self.gl_view.removeItem(grid_item)
                        except Exception:
                            pass
                        self._grid_item = None
                except Exception:
                    pass

                # 清空映射和表格
                try:
                    self._row_to_gl.clear()
                    self._row_to_path.clear()
                    self.table.setRowCount(0)
                except Exception:
                    pass

                # 断开数据源信号，避免悬挂回调
                try:
                    self._data_source.sigDataUpdated.disconnect(self._on_data_updated)
                except Exception:
                    pass

                # 结束当前上下文
                try:
                    self.gl_view.doneCurrent()
                except Exception:
                    pass

            logger.info("PCDViewerWindow: 硬关闭完成，资源已清理")
        except Exception as exc:
            logger.exception("PCDViewerWindow: 硬关闭过程出现异常: %s", exc)
        # 允许 Qt 正常关闭并销毁窗口
        super().closeEvent(event)

    def add_files(self, paths: List[str]) -> None:
        """程序化添加多个 PCD 文件到查看器。"""
        try:
            if not paths:
                return
            added = 0
            for p in paths:
                if self._add_file_internal(p):
                    added += 1
            add_ok_format = self.language_manager.get_constant("pcd_viewer_add_files_success") or "已添加 {count} 个文件"
            self.status.showMessage(add_ok_format.format(count=added), 3000)
            logger.info("PCDViewerWindow: 程序化添加文件数=%d", added)
        except Exception as exc:
            err_format = self.language_manager.get_constant("pcd_viewer_add_files_failed") or "添加文件失败：{error}"
            self.status.showMessage(err_format.format(error=exc), 5000)

    def _add_file_internal(self, p: str) -> bool:
        """内部方法：解析 PCD 并添加到 GL 与表格。"""
        try:
            # 解析文件（支持 ASCII 与二进制）
            pts = PCDLoader.load_pcd(p)
        except Exception as exc:
            err_format = self.language_manager.get_constant("pcd_viewer_parse_failed") or "文件解析失败：{name} - {error}"
            try:
                self.status.showMessage(err_format.format(name=Path(p).name, error=exc), 5000)
            except Exception:
                pass
            logger.exception("解析 PCD 失败: %s", exc)
            return False

        # 颜色与 GL 检查
        color = self._colors[self._color_idx % len(self._colors)]
        self._color_idx += 1
        if not _GL_AVAILABLE:
            try:
                self.status.showMessage(self.language_manager.get_constant("pcd_viewer_gl_unavailable_status") or "GL 不可用，无法绘制点云。", 5000)
            except Exception:
                pass
            return False

        # 添加到 GL 视图
        try:
            self.gl_view.makeCurrent()
            logger.info("PCDViewerWindow: OpenGL context made current for adding item")
        except Exception as ctx_exc:
            logger.error("PCDViewerWindow: Failed to make OpenGL context current: %s", ctx_exc)
            return False
        entry = GLPointCloudEntry(pts, color, size=float(self.size_slider.value()))
        try:
            logger.info("PCDViewerWindow: Adding scatter item to GL view")
            self.gl_view.addItem(entry.scatter)
            logger.info("PCDViewerWindow: Scatter item added successfully")
            try:
                if hasattr(entry.scatter, "initializeGL"):
                    logger.info("PCDViewerWindow: Initializing scatter item GL")
                    entry.scatter.initializeGL()
                    logger.info("PCDViewerWindow: Scatter item GL initialized")
            except Exception as init_exc:
                logger.warning("PCDViewerWindow: Failed to initialize scatter GL: %s", init_exc)
        except Exception as exc:
            err_format = self.language_manager.get_constant("pcd_viewer_add_gl_item_failed") or "添加 GL 绘制项失败：{error}"
            try:
                self.status.showMessage(err_format.format(error=exc), 5000)
            except Exception:
                pass
            logger.exception("PCDViewerWindow: 添加 GL 绘制项失败: %s", exc)
            return False
        finally:
            try:
                self.gl_view.doneCurrent()
                logger.info("PCDViewerWindow: OpenGL context released")
            except Exception as done_exc:
                logger.warning("PCDViewerWindow: Failed to release OpenGL context: %s", done_exc)

        # 更新表格
        try:
            row = self.table.rowCount()
            self.table.insertRow(row)
            chk = QTableWidgetItem("")
            chk.setFlags(Qt.ItemIsUserCheckable | Qt.ItemIsEnabled)
            chk.setCheckState(Qt.Checked)
            self.table.setItem(row, 0, chk)
            full_path = str(Path(p))
            name_item = QTableWidgetItem(Path(p).name)
            name_item.setToolTip(full_path)
            self.table.setItem(row, 1, name_item)
            color_item = QTableWidgetItem("")
            color_item.setFlags(Qt.ItemIsEnabled)
            color_item.setBackground(color)
            color_item.setToolTip(self.language_manager.get_text("pcd_viewer_color_cell_tooltip", "点击以修改点云颜色"))
            self.table.setItem(row, 2, color_item)
            self._row_to_gl[row] = entry
            self._row_to_path[row] = full_path
            try:
                self._update_scene_after_data_change()
            except Exception:
                pass
        except Exception as exc:
            logger.exception("更新 PCDViewer 表格失败: %s", exc)
            return False
        return True

    def _diagnose_opengl_state(self) -> None:
        """诊断OpenGL状态，用于调试打包应用中的渲染问题"""
        try:
            if not hasattr(self, 'gl_view') or not self.gl_view:
                logger.warning("PCDViewerWindow: No GL view available for diagnosis")
                return
                
            logger.info("PCDViewerWindow: Starting OpenGL diagnostics...")
            
            # 检查OpenGL上下文
            try:
                self.gl_view.makeCurrent()
                logger.info("PCDViewerWindow: OpenGL context is valid")
                
                # 获取OpenGL信息
                try:
                    import OpenGL.GL as ogl
                    
                    # 首先清除任何现有错误
                    ogl.glGetError()
                    
                    version = ogl.glGetString(ogl.GL_VERSION)
                    vendor = ogl.glGetString(ogl.GL_VENDOR)
                    renderer = ogl.glGetString(ogl.GL_RENDERER)
                    
                    # 检查获取字符串时是否有错误
                    error = ogl.glGetError()
                    if error == ogl.GL_NO_ERROR:
                        logger.info("PCDViewerWindow: OpenGL Version: %s", version.decode() if version else "Unknown")
                        logger.info("PCDViewerWindow: OpenGL Vendor: %s", vendor.decode() if vendor else "Unknown")
                        logger.info("PCDViewerWindow: OpenGL Renderer: %s", renderer.decode() if renderer else "Unknown")

                        # 检查着色器语言版本
                        sl_version = ogl.glGetString(ogl.GL_SHADING_LANGUAGE_VERSION)
                        logger.info("PCDViewerWindow: GLSL Version: %s", sl_version.decode() if sl_version else "Unknown")

                        # 检查关键的着色器函数是否可用
                        shader_funcs = ["glCreateProgram", "glShaderSource", "glCompileShader", "glAttachShader", "glLinkProgram", "glUseProgram"]
                        for func_name in shader_funcs:
                            if hasattr(ogl, func_name):
                                logger.info(f"PCDViewerWindow: OpenGL function {func_name} is available.")
                            else:
                                logger.warning(f"PCDViewerWindow: Critical OpenGL function {func_name} is MISSING.")
                        
                        logger.info("PCDViewerWindow: No OpenGL errors detected so far.")
                    else:
                        logger.warning("PCDViewerWindow: OpenGL error while getting context info: %s", error)
                        
                except ImportError:
                    logger.warning("PCDViewerWindow: OpenGL.GL not available")
                except Exception as gl_exc:
                    logger.error("PCDViewerWindow: Failed to get OpenGL info: %s", gl_exc)
                
                self.gl_view.doneCurrent()
                
            except Exception as ctx_exc:
                logger.error("PCDViewerWindow: Failed to make OpenGL context current: %s", ctx_exc)
                
            # 检查视图状态
            logger.info("PCDViewerWindow: GLViewWidget visible: %s", self.gl_view.isVisible())
            logger.info("PCDViewerWindow: GLViewWidget enabled: %s", self.gl_view.isEnabled())
            
            # 检查场景中的项目
            try:
                items = self.gl_view.items
                logger.info("PCDViewerWindow: Number of items in scene: %d", len(items))
                for i, item in enumerate(items):
                    logger.info("PCDViewerWindow: Item %d: %s (visible: %s)", 
                              i, type(item).__name__, 
                              getattr(item, 'visible', lambda: True)())
            except Exception as items_exc:
                logger.error("PCDViewerWindow: Failed to enumerate scene items: %s", items_exc)
                
        except Exception as diag_exc:
            logger.error("PCDViewerWindow: OpenGL diagnosis failed: %s", diag_exc)

    def _reset_shader_cache(self) -> None:
        """重置 pyqtgraph 着色器缓存，避免复用旧上下文的 program ID 导致 glUseProgram 错误。

        原理：pyqtgraph.opengl.shaders 模块内部存在全局缓存；通过模块重载清空缓存，
        让新窗口在当前 GL 上下文下重新编译着色器程序。
        """
        try:
            import importlib  # 延迟导入，避免启动阶段过早依赖
            # 确保当前 GL 上下文处于激活状态（有些驱动要求在当前上下文下刷新缓存）
            try:
                if _GL_AVAILABLE and hasattr(self, "gl_view"):
                    self.gl_view.makeCurrent()
            except Exception:
                pass
            # 重载着色器模块以清空缓存
            try:
                import pyqtgraph.opengl.shaders as _shaders  # type: ignore
                importlib.reload(_shaders)
                logger.info("PCDViewerWindow: 已重置着色器缓存（模块重载）")
            except Exception as exc:
                logger.warning("PCDViewerWindow: 着色器缓存重置失败: %s", exc)
        finally:
            try:
                if _GL_AVAILABLE and hasattr(self, "gl_view"):
                    self.gl_view.doneCurrent()
            except Exception:
                pass

    def _on_table_item_changed(self, item: QTableWidgetItem) -> None:
        """复选框变更：控制点云显示/隐藏。"""
        try:
            if item.column() != 0:
                return
            row = item.row()
            entry = self._row_to_gl.get(row)
            if entry is None:
                return
            entry.set_visible(item.checkState() == Qt.Checked)
        except Exception:
            pass

    def _on_table_cell_clicked(self, row: int, column: int) -> None:
        """点击颜色单元格：弹出颜色对话框并更新点云颜色。"""
        try:
            if column != 2:
                return
            entry = self._row_to_gl.get(row)
            if entry is None:
                return
            # 当前颜色作为初始值
            current_bg = self.table.item(row, column)
            initial = current_bg.background().color() if current_bg is not None else QColor(255, 255, 255)
            color_dlg_title = self.language_manager.get_constant("pcd_viewer_color_dialog_title") or self.language_manager.get_text("pcd_viewer_color_dialog_title", "选择点云颜色")
            color = QColorDialog.getColor(initial, self, color_dlg_title)
            if not color.isValid():
                return
            entry.set_color(color)
            cell = self.table.item(row, column)
            if cell is not None:
                cell.setBackground(color)
        except Exception:
            pass

    def _on_point_size_changed(self, value: int) -> None:
        """点大小滑块变更：更新所有 GL 点云尺寸。"""
        try:
            for entry in self._row_to_gl.values():
                entry.set_size(float(value))
            self._update_status()
        except Exception:
            pass

    def _on_color_map_changed(self, index: int) -> None:
        """颜色映射切换：统一颜色 / 按Z映射。"""
        try:
            # 使用索引驱动逻辑，避免依赖本地化文本
            mode = "uniform" if int(index) == 0 else "map_z"
            for entry in self._row_to_gl.values():
                entry.apply_color_map(mode)
            self._update_status()
        except Exception:
            pass

    # ------------------------ 辅助方法 ------------------------
    def _update_status(self) -> None:
        """更新状态栏显示信息。"""
        try:
            files = len(self._row_to_gl)
            size = self.size_slider.value()
            # 使用格式化语言键生成状态栏文本
            fmt = self.language_manager.get_constant("pcd_viewer_status_format") or "文件: {files} | 点大小: {size} | 颜色映射: {mode}"
            self.status.showMessage(fmt.format(files=files, size=size, mode=self.color_map_combo.currentText()))
        except Exception:
            pass

    def _on_language_changed(self, _lang: dict) -> None:
        """语言变更回调：更新窗口标题、表头与状态栏文本。"""
        try:
            # 更新窗口标题
            self.setWindowTitle(self.language_manager.get_text("pcd_viewer_window_title", "PCD 查看器"))
            # 更新表头
            headers = self.language_manager.get_text("pcd_viewer_table_headers", ["显示", "文件名", "颜色"])
            if isinstance(headers, list) and len(headers) == 3:
                self.table.setHorizontalHeaderLabels(headers)
            # 刷新状态栏
            self._update_status()
        except Exception:
            pass


__all__ = [
    "PCDLoader",
    "PCDViewerWindow",
    "GLPointCloudEntry",
]
